Analyse comparative des performances de la propriété CSS @

Publié le 2 octobre 2024

Lorsque vous commencez à utiliser une nouvelle fonctionnalité CSS, il est important de comprendre son impact sur les performances de vos sites Web, qu'il soit positif ou négatif. @property est désormais disponible dans la version de référence. Cet article explore son impact sur les performances et les mesures que vous pouvez prendre pour éviter tout impact négatif.

Analyse comparative des performances du CSS avec PerfTestRunner

Pour évaluer les performances du CSS, nous avons créé la suite de tests "CSS Selector Benchmark". Basé sur la version PerfTestRunner de Chromium, il évalue l'impact du CSS sur les performances. C'est ce PerfTestRunner que Blink, le moteur de rendu sous-jacent de Chromium, utilise pour ses tests de performances internes.

Le programme d'exécution inclut une méthode measureRunsPerSecond qui est utilisée pour les tests. Plus le nombre d'exécutions par seconde est élevé, mieux c'est. Un benchmark measureRunsPerSecond de base avec cette bibliothèque se présente comme suit:

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

Chaque option pour measureRunsPerSecond est décrite par des commentaires dans le bloc de code, la fonction run étant la partie principale qui est mesurée.

Les benchmarks des sélecteurs CSS nécessitent un arbre DOM

Étant donné que les performances des sélecteurs CSS dépendent également de la taille du DOM, ces benchmarks nécessitent un arbre DOM de taille raisonnable. Au lieu de créer manuellement cette arborescence DOM, elle est générée.

Par exemple, la fonction makeTree suivante fait partie des benchmarks @property. Il construit une arborescence de 1 000 éléments, chacun contenant des enfants imbriqués.

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

Étant donné que les benchmarks de sélecteur CSS ne modifient pas l'arborescence DOM, cette génération d'arborescence n'est exécutée qu'une seule fois, avant l'exécution des benchmarks.

Exécuter un benchmark

Pour exécuter un benchmark qui fait partie de la suite de tests, vous devez d'abord démarrer un serveur Web :

npm run start

Une fois le benchmark lancé, vous pouvez accéder à son URL publiée et exécuter window.startTest() manuellement.

Pour exécuter ces benchmarks de manière isolée, sans aucune extension ni autre facteur d'intervention, Puppeteer est déclenché à partir de la CLI pour charger et exécuter le benchmark transmis.

Pour ces benchmarks @property spécifiques, plutôt que d'accéder à la page correspondante à son URL http://localhost:3000/benchmarks/at-rule/at-property.html, appelez les commandes suivantes dans la CLI :

npm run benchmark at-rule/at-property

La page est alors chargée via Puppeteer, appelle automatiquement window.startTest() et renvoie les résultats.

Analyse comparative des performances des propriétés CSS

Pour évaluer les performances d'une propriété CSS, vous devez évaluer la rapidité avec laquelle elle peut gérer une invalidation de style et la tâche de recalcul de style ultérieure que le navigateur doit effectuer.

L'invalidation d'un style consiste à indiquer les éléments dont le style doit être recalculé en réponse à une modification du DOM. L'approche la plus simple consiste à invalider tout en réponse à chaque modification.

Dans ce cas, il faut faire la distinction entre les propriétés CSS qui héritent et celles qui ne le font pas.

  • Lorsqu'une propriété CSS héritée est modifiée sur un élément ciblé, les styles de tous les éléments du sous-arbre sous l'élément ciblé doivent également être modifiés.
  • Lorsqu'une propriété CSS qui n'hérite pas de modifications sur un élément ciblé, seuls les styles de cet élément individuel sont invalidés.

Il serait injuste de comparer des propriétés qui héritent à des propriétés qui ne le font pas. Vous devez donc exécuter deux ensembles de benchmarks :

  • Ensemble de benchmarks avec des propriétés héritées.
  • Ensemble d'analyses comparatives dont les propriétés n'héritent pas.

Il est important de choisir soigneusement les établissements à comparer. Alors que certaines propriétés (telles que accent-color) n'invalident que les styles, de nombreuses propriétés (telles que writing-mode) invalident également d'autres éléments, tels que la mise en page ou la peinture. Vous souhaitez les propriétés qui n'invalident que les styles.

Pour le déterminer, consultez la liste des propriétés CSS de Blink. Chaque propriété possède un champ invalidate qui indique ce qui est invalidé.

De plus, il est également important de choisir une propriété qui n'est pas marquée comme independent dans cette liste, car une analyse comparative de cette propriété fausserait les résultats. Les propriétés indépendantes n'ont aucun effet secondaire sur d'autres propriétés ou indicateurs. Lorsque seules les propriétés indépendantes ont changé, Blink utilise un chemin de code rapide qui clone le style du descendant et met à jour les nouvelles valeurs dans cette copie clonée. Cette approche est plus rapide qu'un recalcul complet.

Analyse comparative des performances des propriétés CSS qui héritent

Le premier ensemble de benchmarks se concentre sur les propriétés CSS qui héritent. Il existe trois types de propriétés qui héritent pour tester et comparer les unes aux autres :

  • Propriété standard qui hérite : accent-color.
  • Propriété personnalisée non enregistrée : --unregistered.
  • Une propriété personnalisée enregistrée avec inherits: true: --registered.

Les propriétés personnalisées non enregistrées sont ajoutées à cette liste, car elles héritent par défaut.

Comme indiqué précédemment, la propriété qui hérite d'une propriété qui en hérite a été soigneusement sélectionnée afin qu'elle n'invalide que les styles et qu'elle ne soit pas marquée comme independent.

Pour les propriétés personnalisées enregistrées, seules celles dont le descripteur inherits est défini sur "true" sont testées lors de cette exécution. Le descripteur inherits détermine si la propriété hérite des enfants ou non. Peu importe que cette propriété soit enregistrée via CSS @property ou JavaScript CSS.registerProperty, car l'enregistrement lui-même ne fait pas partie de l'analyse comparative.

Les benchmarks

Comme indiqué précédemment, la page contenant les benchmarks commence par construire une arborescence DOM afin que la page dispose d'un ensemble suffisamment important de nœuds pour voir l'impact des modifications.

Chaque benchmark modifie la valeur d'une propriété, puis déclenche une invalidation de style. Le benchmark mesure essentiellement le temps nécessaire au prochain calcul de la page pour réévaluer tous ces styles invalidés.

Une fois qu'un benchmark unique est terminé, tous les styles injectés sont réinitialisés afin que le benchmark suivant puisse commencer.

Par exemple, le benchmark qui mesure les performances de la modification du style de --registered se présente comme suit:

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

Les benchmarks qui testent les autres types de propriétés fonctionnent de la même manière, mais ont un bootstrap vide, car il n'y a pas de propriété à enregistrer.

Les résultats

L'exécution de ces analyses comparatives avec 20 itérations sur un MacBook Pro (Apple M1 Pro) de 2021 avec 16 Go de RAM donne les moyennes suivantes:

  • Propriété standard qui hérite (accent-color) : 163 exécutions par seconde (soit 6,13 ms par exécution)
  • Propriété personnalisée non enregistrée (--unregistered) : 256 exécutions par seconde (soit 3,90 ms par exécution)
  • Propriété personnalisée enregistrée avec inherits: true (--registered): 252 exécutions par seconde (soit 3,96 ms par exécution)

Lors de plusieurs exécutions, les analyses comparatives donnent des résultats similaires.

Graphique à barres avec les résultats des propriétés qui héritent. Plus le nombre est élevé, plus les performances sont rapides.
Figure: Graphique à barres avec les résultats des propriétés héritées. Plus le nombre est élevé, plus les performances sont rapides.

Les résultats montrent que le coût de l'enregistrement d'une propriété personnalisée est très faible comparé à l'enregistrement de la propriété personnalisée. Les propriétés personnalisées enregistrées qui héritent de l'exécution s'exécutent à 98% de la vitesse des propriétés personnalisées non enregistrées. En termes absolus, l'enregistrement de la propriété personnalisée ajoute un surcoût de 0,06 ms.

Effectuer une analyse comparative des performances des propriétés CSS qui n'héritent pas

Les prochaines propriétés de l'analyse comparative sont celles qui n'héritent pas. Il n'existe que deux types de propriétés pouvant être comparés :

  • Propriété standard qui n'hérite pas: z-index.
  • Propriété personnalisée enregistrée avec inherits: false : --registered-no-inherit.

Les propriétés personnalisées non enregistrées ne peuvent pas faire partie de ce benchmark, car elles héritent toujours.

Les benchmarks

Les benchmarks sont très similaires aux scénarios précédents. Pour le test avec --registered-no-inherit, l'enregistrement de propriété suivant est injecté dans la phase bootstrap du benchmark :

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

Les résultats

Exécuter ces benchmarks avec 20 itérations sur un MacBook Pro 2021 (Apple M1 Pro) avec 16 Go de RAM donne les moyennes suivantes :

  • Propriété régulière qui n'hérite pas : 290 269 exécutions par seconde (soit 3,44 µs par exécution)
  • Propriété personnalisée enregistrée qui n'hérite pas : 214 110 exécutions par seconde (soit 4,67 µs par exécution)

Le test a été répété plusieurs fois, et voici les résultats typiques.

Graphique à barres présentant les résultats pour les propriétés qui ne sont pas héritées. Plus le nombre est élevé, plus les performances sont rapides.
Figure: Graphique à barres affichant les résultats pour les propriétés n'héritant pas. Plus le nombre est élevé, plus les performances sont rapides.

Ce qui ressort ici, c'est que les propriétés qui n'héritent pas sont beaucoup plus rapides que celles qui héritent. Bien que cela soit prévisible pour les propriétés standards, cela est également vrai pour les propriétés personnalisées.

  • Pour les propriétés standards, le nombre d'exécutions est passé de 163 exécutions par seconde à plus de 290 000 exécutions par seconde, soit une augmentation de 1 780 % des performances.
  • Pour les propriétés personnalisées, le nombre d'exécutions est passé de 252 exécutions par seconde à plus de 214 000 exécutions par seconde, soit une augmentation de 848 % des performances.

Ce qu'il faut retenir, c'est que l'utilisation de inherits: false lors de l'enregistrement d'une propriété personnalisée a un impact significatif. Si vous pouvez enregistrer votre propriété personnalisée avec inherits: false, nous vous recommandons vivement de le faire.

Étalonnage bonus : plusieurs enregistrements de propriétés personnalisées

Un autre élément intéressant à comparer est l'impact d'un grand nombre d'enregistrements de propriétés personnalisées. Pour ce faire, exécutez à nouveau le test avec --registered-no-inherit,qui effectue 25 000 autres enregistrements de propriétés personnalisées à l'avance. Ces propriétés personnalisées sont utilisées sur :root.

Ces enregistrements sont effectués à l'étape setup de l'analyse comparative :

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

Le nombre d'exécutions par seconde pour ce benchmark est très similaire au résultat pour "Propriété personnalisée enregistrée qui n'hérite pas" (214 110 exécutions par seconde contre 213 158 exécutions par seconde), mais ce n'est pas ce qui est intéressant. Après tout, il est normal que la modification d'une propriété personnalisée ne soit pas affectée par les enregistrements d'autres propriétés.

L'aspect intéressant de ce test est de mesurer l'impact des enregistrements eux-mêmes. En passant aux outils de développement, vous constatez que 25 000 enregistrements de propriétés personnalisées ont un coût initial de recalcul du style légèrement supérieur à 30ms. Une fois cette opération effectuée, la présence de ces enregistrements n'a plus d'effet.

Capture d&#39;écran de DevTools avec le coût de &quot;Recalculate Style&quot; (Recalculer le style) pour effectuer 25 000 enregistrements de propriétés personnalisées mis en évidence. L&#39;info-bulle indique que l&#39;opération a pris 32,42 ms.
Figure : Capture d'écran des outils de développement avec le coût de la fonctionnalité "Recalculer le style" pour 25 000 enregistrements de propriétés personnalisées mis en évidence. L'info-bulle indique qu'il a fallu 32.42ms

Conclusion et points à retenir

En résumé, il y a trois points à retenir de tout cela:

  • L'enregistrement d'une propriété personnalisée avec @property entraîne un léger coût de performances. Ce coût est souvent négligeable, car en enregistrant des propriétés personnalisées, vous développez tout leur potentiel, ce qui n'est pas possible sans cela.

  • L'utilisation de inherits: false lors de l'enregistrement d'une propriété personnalisée a un impact significatif. Vous empêchez ainsi l'héritage de la propriété. Par conséquent, lorsque la valeur de la propriété change, elle n'affecte que les styles de l'élément correspondant, et non l'ensemble du sous-arbre.

  • Le nombre d'enregistrements @property n'a aucune incidence sur le recalcul des styles. Les enregistrements ne vous coûtent que très peu de frais à l'avance, mais une fois cette opération effectuée, vous n'avez rien d'autre à faire.